Skip to content

Direct browser-to-browser transfers over WebRTC with relay fallback - #42

Merged
simkim merged 1 commit into
masterfrom
features/lan-p2p-browser-transfer-v1
Aug 24, 2026
Merged

Direct browser-to-browser transfers over WebRTC with relay fallback#42
simkim merged 1 commit into
masterfrom
features/lan-p2p-browser-transfer-v1

Conversation

@simkim

@simkim simkim commented Aug 24, 2026

Copy link
Copy Markdown
Owner

Devices behind the same public IP are usually on the same LAN, yet every
byte went up the uplink and back down through the server. Clicking
Download now first tries a WebRTC DataChannel to the sender, signaled
over the existing WebSocket, and silently falls back to GET /share/:uuid
if the channel isn't open within 5 seconds.

Security model, unchanged trust boundary:

  • a signaling session can only be opened toward the owner of a share in
    the requester's own namespace (same authorization as /share/:uuid);
    session ids are minted by the server, client identities never exposed
  • only LAN host candidates are relayed (mDNS names or private/link-local
    addresses), no STUN/TURN, never a public address; enforced on both
    sides, SDP scrubbed server-side
  • all signaling inputs validated and size-capped, sessions and
    candidates capped per client, cleanup on disconnect
  • the channel is DTLS-encrypted end to end; the server never sees content

A service worker (public/sw.js) streams received chunks straight into a
regular browser download, so size isn't bounded by memory; without it
small files are saved from a Blob and big ones use the relay.

Also: share uuids now come from crypto.randomUUID() (they're capability
tokens), specs pin how the client IP is resolved behind the proxy, and
the relay fallback uses an anchor click rather than a navigation, which
made Firefox/Safari drop the WebSocket.

Devices behind the same public IP are usually on the same LAN, yet every
byte went up the uplink and back down through the server. Clicking
Download now first tries a WebRTC DataChannel to the sender, signaled
over the existing WebSocket, and silently falls back to GET /share/:uuid
if the channel isn't open within 5 seconds.

Security model, unchanged trust boundary:
- a signaling session can only be opened toward the owner of a share in
  the requester's own namespace (same authorization as /share/:uuid);
  session ids are minted by the server, client identities never exposed
- only LAN host candidates are relayed (mDNS names or private/link-local
  addresses), no STUN/TURN, never a public address; enforced on both
  sides, SDP scrubbed server-side
- all signaling inputs validated and size-capped, sessions and
  candidates capped per client, cleanup on disconnect
- the channel is DTLS-encrypted end to end; the server never sees content

A service worker (public/sw.js) streams received chunks straight into a
regular browser download, so size isn't bounded by memory; without it
small files are saved from a Blob and big ones use the relay.

Also: share uuids now come from crypto.randomUUID() (they're capability
tokens), specs pin how the client IP is resolved behind the proxy, and
the relay fallback uses an anchor click rather than a navigation, which
made Firefox/Safari drop the WebSocket.
@simkim
simkim merged commit bafe7ad into master Aug 24, 2026
1 check passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant